/*
@Time   : 2022/5/10 17:38
@Author : ckx0709
@Remark :
*/
package main

import (
	"bytes"
	"flag"
	"fmt"
	"github.com/fatih/structtag"
	"go/ast"
	"go/printer"
	"go/token"
	"go/types"
	"golang.org/x/tools/go/packages"
	"io/ioutil"
	"log"
	"os"
	"path/filepath"
	"reflect"
	"regexp"
	"strings"
	"text/template"
)

const (
	funGetter = "get"
	funSetter = "set"
	tagName   = "gorm"
)

var (
	columnCompile  = regexp.MustCompile("column:([\\w]+);?")
	autoFunc       = []string{"set"}
	GetterTemplate = `
	func ({{.Receiver}} *{{.Struct}}) Get{{.Field}}() {{.Type}} {
		return {{.Receiver}}.{{.Field}}
	}
	`
	SetterTemplate = `
	func ({{.Receiver}} *{{.Struct}}) Set{{.Field}}(val {{.Type}}) {
		{{.Receiver}}.{{.Field}} = val
		//{{.Receiver}}.Update("{{.Column}}",val)
	}`

	output        = flag.String("output", "", "output file name; default srcdir/<type>_sloth.go")
	structNames   = []string{"IntSliceHeader"}
	packageConfig *packages.Config
	outDir        string
)

type StructFieldInfoArr = []StructFieldInfo

type StructFieldInfo struct {
	Name   string
	Type   string
	Access []string
	column string
}

type Generator struct {
	buf        map[string]*bytes.Buffer // Accumulated output.
	pkg        *Package                 // Package we are scanning.
	structInfo map[string]StructFieldInfoArr
	walkMark   map[string]bool
}

func (g *Generator) Printf(structName, format string, args ...interface{}) {
	buf, ok := g.buf[structName]
	if !ok {
		buf = bytes.NewBufferString("")
		g.buf[structName] = buf
	}
	fmt.Fprintf(buf, format, args...)
}

type Package struct {
	name  string
	defs  map[*ast.Ident]types.Object
	files []*File
}

type File struct {
	pkg     *Package  // Package to which this file belongs.
	file    *ast.File // Parsed AST.
	fileSet *token.FileSet
	// These fields are reset for each type being generated.
	structNames []string // Name of the constant type.
	path        string
}

func init() {
	packageConfig = &packages.Config{
		Mode:  packages.LoadAllSyntax,
		Tests: false,
	}
}

func main() {
	pats := []string{"go_iteration/book/vrox_go/unit1"}
	load, err := packages.Load(packageConfig, pats...)
	if err != nil {
		fmt.Printf("err :%+v", err)
		return
	}

	loadPkg := load[0]
	gen := &Generator{
		buf: make(map[string]*bytes.Buffer),
		pkg: &Package{
			name:  loadPkg.Name,
			defs:  loadPkg.TypesInfo.Defs,
			files: make([]*File, len(loadPkg.Syntax)),
		},
		structInfo: nil,
		walkMark:   make(map[string]bool),
	}

	//filepath.Dir()
	outDir, _ = filepath.Abs(loadPkg.PkgPath[13:])
	//fmt.Printf("[packagePath] %s\n", packageAbsPAth)

	for i, file := range loadPkg.Syntax {
		fmt.Printf("[loadPkg.Syntax] file  :%+v\n", file)
		gen.pkg.files[i] = &File{
			pkg:     gen.pkg,
			file:    file,
			fileSet: loadPkg.Fset,
			path:    outDir,
		}
	}

	//structStr := "IntSliceHeader"
	for _, file := range gen.pkg.files {
		file.structNames = structNames
	}

	for _, file := range gen.pkg.files {
		fmt.Printf("fileType :%s\n", file.structNames)

		structMap, err := ParseStruct(file.file, file.fileSet, tagName)
		if err != nil {
			fmt.Println("失败:" + err.Error())
			return
		}

		fmt.Printf("structMap := %+v\n", structMap)
		for stName, info := range structMap {
			gen.Printf(stName, "// Code generated by \"sloth %s\"; DO NOT EDIT.\n", strings.Join(os.Args[1:], " "))
			gen.Printf(stName, "\n")
			gen.Printf(stName, "package %s\n", gen.pkg.name)
			gen.Printf(stName, "\n")

			for _, field := range info {
				for _, access := range autoFunc {
					switch access {
					case funSetter:
						gen.Printf(stName, "%s\n", genSetter(stName, field.Name, field.column, field.Type))
					case funGetter:
						gen.Printf(stName, "%s\n", genGetter(stName, field.Name, field.Type))
					}
				}
			}
		}

	}

	outFiled(gen)

}

func outFiled(gen *Generator) {
	outputName := *output
	for _, strName := range structNames {
		if outputName == "" {
			baseName := fmt.Sprintf("%s_sloth.go", strName)
			outputName = filepath.Join(outDir, "\\", strings.ToLower(baseName))
			//outputName = strings.Replace(outputName, `\`, "/", -1)
		}
		buf, ok := gen.buf[strName]
		if !ok {
			panic(fmt.Sprintf("generate struct %s failed.", strName))
		}
		var context = (buf).Bytes()
		err := ioutil.WriteFile(outputName, context, 0644)
		if err != nil {
			log.Fatalf("writing output: %s", err)
		}
	}
}

func ParseStruct(file *ast.File, fileSet *token.FileSet, tagName string) (structMap map[string]StructFieldInfoArr, err error) {
	structMap = make(map[string]StructFieldInfoArr)

	collectStructs := func(x ast.Node) bool {
		ts, ok := x.(*ast.TypeSpec)
		if !ok || ts.Type == nil {
			return true
		}

		// 获取结构体名称
		structName := ts.Name.Name

		//这简直就是脱裤子放屁
		s, ok := ts.Type.(*ast.StructType)
		if !ok {
			return true
		}
		fileInfos := make([]StructFieldInfo, 0)
		for _, field := range s.Fields.List {
			kind := reflect.TypeOf(file).Kind()
			if kind == reflect.Struct || kind == reflect.Array || kind == reflect.UnsafePointer || len(field.Names) == 0 {
				continue
			}
			name := field.Names[0].Name
			info := StructFieldInfo{Name: name}
			var typeNameBuf bytes.Buffer
			err := printer.Fprint(&typeNameBuf, fileSet, field.Type)
			if err != nil {
				fmt.Println("获取类型失败:", err)
				return true
			}
			info.Type = typeNameBuf.String()
			if field.Tag != nil { // 有tag
				tag := field.Tag.Value
				tag = strings.Trim(tag, "`")
				tags, err := structtag.Parse(tag)
				if err != nil {
					return true
				}
				filedTag, err := tags.Get(tagName)
				submatch := columnCompile.FindStringSubmatch(filedTag.Value())
				if len(submatch) > 0 {
					info.column = submatch[1]
				}

			} else {
				firstChar := name[0:1]
				if strings.ToUpper(firstChar) == firstChar { //大写
					info.Access = []string{funGetter, funSetter}
				} else { // 小写
					info.Access = []string{funGetter}
				}
			}
			fileInfos = append(fileInfos, info)
		}
		structMap[structName] = fileInfos
		return false
	}

	ast.Inspect(file, collectStructs)

	return structMap, nil
}

func genSetter(structName, fieldName, column, typeName string) string {
	t := template.New("setter")
	t = template.Must(t.Parse(SetterTemplate))
	res := bytes.NewBufferString("")
	t.Execute(res, map[string]string{
		"Receiver": strings.ToLower(structName[0:1]),
		"Struct":   structName,
		"Field":    fieldName,
		"Type":     typeName,
		"Column":   column,
	})
	return res.String()
}

func genGetter(structName, fieldName, typeName string) string {
	t := template.New("getter")
	t = template.Must(t.Parse(GetterTemplate))
	res := bytes.NewBufferString("")
	t.Execute(res, map[string]string{
		"Receiver": strings.ToLower(structName[0:1]),
		"Struct":   structName,
		"Field":    fieldName,
		"Type":     typeName,
	})
	return res.String()
}
